package gestordeentidades.identitymap;

import gestordeentidades.identitymap.interfaces.IEntityMap;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import gestordeentidades.persistance.DataBaseObject;
import gestordeentidades.persistance.interfaces.IPersistance;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * Trabalho DAS - 2014/2015 Objecto que tem a responsabilidade de assegurar que
 * os objectos são lidos apenas quando necessário (uma única vez). Mantém um
 * mapa que permite pesquisar aos objectos que já se encontram em memória.
 *
 * @author a21210380, a21190325, a21210392
 */
public class EntityMap implements IEntityMap {

    private static final Logger logger = Logger.getLogger(EntityMap.class.getName());

    private final IPersistance persistance;
    private final HashMap<Class, Entidade> entityMap;

    public EntityMap(IPersistance persistance) {
        if (persistance == null) {
            throw new NullPointerException("O persistance não pode ser null!");
        }

        this.persistance = persistance;
        this.entityMap = new HashMap<>();
    }

    /**
     * Inicia a entidade para que seja possivel procurar por este tipo de
     * entidade
     *
     * @param <T> extends DataBaseObject para se ter acesso ao id do objecto
     * @param entidade
     * @return EntityMap para fazer chain
     */
    public <T extends DataBaseObject> EntityMap iniciarEntidade(Class<T> entidade) {
        if (entidade == null) {
            throw new NullPointerException("A entidade não pode ser null!");
        }
        if (entityMap.containsKey(entidade)) {
            logger.logp(Level.WARNING, EntityMap.class.getName(), "iniciarEntidade()", "A entidade " + entidade + " já se encontra inicializada!");
            return this;
        }
        entityMap.put(entidade, new Entidade());
        return this;
    }

    public EntityMap loadAll() {
        entityMap.forEach((c, m) -> {
            m.load(c);
        });
        return this;
    }

    @Override
    public <T extends DataBaseObject> T procurarEntidadePeloID(Class<T> c, int id) {
        if (c == null) {
            throw new NullPointerException("A class não pode ser null!");
        }
        if (entityMap.containsKey(c)) {
            return (T) entityMap.get(c).procurarEntidadePeloID(c, id);
        } else {
            logger.logp(Level.WARNING, EntityMap.class.getName(), "procurarEntidadePeloID()", "A entidade " + c + " não se encontra inicializada!");
            return null;
        }
    }

    @Override
    public <T extends DataBaseObject> Collection<T> procurarPorTodasAsEntidadesComoColecao(Class<T> c) {
        if (c == null) {
            throw new NullPointerException("A class não pode ser null!");
        }
        if (entityMap.containsKey(c)) {
            return entityMap.get(c).procurarPorTodasAsEntidadesComoColecao();
        } else {
            logger.logp(Level.WARNING, EntityMap.class.getName(), "procurarPorTodasAsEntidadesComoLista()", "A entidade " + c + " não se encontra inicializada!");
            return null;
        }
    }

    @Override
    public <T extends DataBaseObject> Map<Integer, T> procurarPorTodasAsEntidadesComoMapa(Class<T> c) {
        if (c == null) {
            throw new NullPointerException("A class não pode ser null!");
        }
        if (entityMap.containsKey(c)) {
            return entityMap.get(c).procurarPorTodasAsEntidadesComoMapa();
        } else {
            logger.logp(Level.WARNING, EntityMap.class.getName(), "procurarPorTodasAsEntidadesComoMapa()", "A entidade " + c + " não se encontra inicializada!");
            return null;
        }
    }

    @Override
    public <T extends DataBaseObject> void registarNovaEntidade(T entidade) {
        if (entidade == null) {
            throw new NullPointerException("A entidade não pode ser null!");
        }
        Class c = entidade.getClass();
        if (entityMap.containsKey(c)) {
            entityMap.get(c).registarNovaEntidade(entidade);
        } else {
            logger.logp(Level.WARNING, EntityMap.class.getName(), "registarNovaEntidade()", "A entidade " + entidade + " não se encontra inicializada!");
        }
    }

    @Override
    public <T extends DataBaseObject> void registarEntidadeApagada(T entidade) {
        if (entidade == null) {
            throw new NullPointerException("A entidade não pode ser null!");
        }
        Class c = entidade.getClass();
        if (entityMap.containsKey(c)) {
            entityMap.get(c).registarEntidadeApagada(entidade);
        } else {
            logger.logp(Level.WARNING, EntityMap.class.getName(), "registarEntidadeApagada()", "A entidade " + entidade + " não se encontra inicializada!");
        }
    }

    @Override
    public <T extends DataBaseObject> List<T> filtrarEntidades(Class<T> entidade, Predicate<T> filtro) {
        if (entidade == null) {
            throw new NullPointerException("A entidade não pode ser null!");
        }
        if (filtro == null) {
            throw new NullPointerException("O filtro não pode ser null!");
        }
        if (entityMap.containsKey(entidade)) {
            return entityMap.get(entidade).filtrarEntidades(filtro);
        } else {
            logger.logp(Level.WARNING, EntityMap.class.getName(), "procurarPorTodasAsEntidadesComoMapa()", "A entidade " + entidade + " não se encontra inicializada!");
            return null;
        }
    }

    private class Entidade<T extends DataBaseObject> {

        private final HashMap<Integer, T> mapa = new HashMap<>();

        public void load(Class<T> c) {
            Map patch = persistance.procurarPorTodasAsEntidadesComoMapa(c, EntityMap.this);
            if (patch != null) {
                // remover todas as keys que já se encontram no entitymap
                patch.keySet().removeAll(mapa.keySet());
                // fazer inserção das que não estão
                mapa.putAll(patch);
                
                // Faz replace se já existirem, efeito que não se quer
                //mapa.putAll(procurarPorTodasAsEntidadesComoMapa);
            }
        }

        public List<T> filtrarEntidades(Predicate<T> predicate) {
            return mapa.values()
                    .stream()
                    .filter(predicate)
                    .collect(Collectors.toList());
        }

        public T procurarEntidadePeloID(Class<T> c, int id) {
            if (mapa.containsKey(id)) {
                return mapa.get(id);
            } else {
                //logger.logp(Level.INFO, EntityMap.class.getName(), "procurarEntidadePeloID()", "A entidade " + c + " id=" + id + " não foi encontrada no entitymap, vai ser procurada na persistance!");
                T findById = persistance.procurarEntidadePeloID(c, EntityMap.this, id);
                if (findById != null) {
                    //logger.logp(Level.INFO, EntityMap.class.getName(), "procurarEntidadePeloID()", "A entidade " + c + " id=" + id + " foi encontrada na persistance!");
                    mapa.put(id, findById);
                    return findById;
                }
                logger.logp(Level.SEVERE, EntityMap.class.getName(), "procurarEntidadePeloID()", "A entidade " + c + " id=" + id + " não foi encontrada na persistance!");
                return null;
            }
        }

        public Collection<T> procurarPorTodasAsEntidadesComoColecao() {
            return Collections.unmodifiableCollection(mapa.values());
        }

        public Map<Integer, T> procurarPorTodasAsEntidadesComoMapa() {
            return Collections.unmodifiableMap(mapa);
        }

        public void registarNovaEntidade(T entidade) {
            if (!mapa.containsKey(entidade.getId())) {
                mapa.put(entidade.getId(), entidade);
            } else {
                logger.logp(Level.WARNING, EntityMap.class.getName(), "registarNovaEntidade()", "A entidade " + entidade + " já tem se encontra registada!");
            }
        }

        public void registarEntidadeApagada(T entidade) {
            if (mapa.containsKey(entidade.getId())) {
                mapa.remove(entidade.getId());
            } else {
                logger.logp(Level.WARNING, EntityMap.class.getName(), "Entidade-registarEntidadeApagada()", "A entidade " + entidade + " não se encontrava registada!");
            }
        }
    }
}
